Peter pointed out that stringByExpandingTildeInPath was unneeded since path returns...
[adiumx.git] / Frameworks / Adium Framework / Source / AIChat.m
blobde70534ff85cce2e2eed3d1a405226d63ad3ce6e
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
16 #import <Adium/AIAccount.h>
17 #import <Adium/AIChat.h>
18 #import <Adium/AIContentMessage.h>
19 #import <Adium/AIListContact.h>
20 #import <Adium/ESFileTransfer.h>
21 #import <Adium/AIHTMLDecoder.h>
22 #import <Adium/AIServiceIcons.h>
23 #import <Adium/AIUserIcons.h>
25 #import <Adium/AIContactControllerProtocol.h>
26 #import <Adium/AIContentControllerProtocol.h>
27 #import <Adium/AIChatControllerProtocol.h>
28 #import <Adium/AIInterfaceControllerProtocol.h>
29 #import <Adium/AIPreferenceControllerProtocol.h>
31 #import <AIUtilities/AIArrayAdditions.h>
32 #import <AIUtilities/AIMutableOwnerArray.h>
34 #import "AIMessageWindowController.h"
35 #import "AIMessageWindow.h"
36 #import "AIInterfaceControllerProtocol.h"
37 #import "AIWebKitMessageViewController.h"
40 @interface AIChat (PRIVATE)
41 - (id)initForAccount:(AIAccount *)inAccount;
42 - (void)clearUniqueChatID;
43 - (void)clearListObjectStatuses;
44 @end
46 @implementation AIChat
48 static int nextChatNumber = 0;
50 + (id)chatForAccount:(AIAccount *)inAccount
52     return [[[self alloc] initForAccount:inAccount] autorelease];
55 - (id)initForAccount:(AIAccount *)inAccount
57     if ((self = [super init])) {
58                 name = nil;
59                 account = [inAccount retain];
60                 participatingListObjects = [[NSMutableArray alloc] init];
61                 dateOpened = [[NSDate date] retain];
62                 uniqueChatID = nil;
63                 ignoredListContacts = nil;
64                 isOpen = NO;
65                 isGroupChat = NO;
66                 expanded = YES;
67                 customEmoticons = nil;
68                 hasSentOrReceivedContent = NO;
69                 pendingOutgoingContentObjects = [[NSMutableArray alloc] init];
71                 AILog(@"[AIChat: %x initForAccount]",self);
72         }
74     return self;
77 /*!
78  * @brief Deallocate
79  */
80 - (void)dealloc
82         AILog(@"[%@ dealloc]",self);
84         [account release];
85         [participatingListObjects release];
86         [dateOpened release];
87         [ignoredListContacts release];
88         [pendingOutgoingContentObjects release];
89         [uniqueChatID release]; uniqueChatID = nil;
90         [customEmoticons release]; customEmoticons = nil;
92         [super dealloc];
95 //Big image
96 - (NSImage *)chatImage
98         AIListContact   *listObject = [self listObject];
99         NSImage                 *image = nil;
101         if (listObject) {
102                 image = [[listObject parentContact] userIcon];
103                 if (!image) image = [AIServiceIcons serviceIconForObject:listObject type:AIServiceIconLarge direction:AIIconNormal];
104         } else {
105                 image = [AIServiceIcons serviceIconForObject:[self account] type:AIServiceIconLarge direction:AIIconNormal];
106         }
108         return image;
111 //lil image
112 - (NSImage *)chatMenuImage
114         AIListObject    *listObject;
115         NSImage                 *chatMenuImage = nil;
116         
117         if ((listObject = [self listObject])) {
118                 chatMenuImage = [AIUserIcons menuUserIconForObject:listObject];
119         }
121         return chatMenuImage;
125 //Associated Account ---------------------------------------------------------------------------------------------------
126 #pragma mark Associated Account
127 - (AIAccount *)account
129     return account;
132 - (void)setAccount:(AIAccount *)inAccount
134         if (inAccount != account) {
135                 [account release];
136                 account = [inAccount retain];
137                 
138                 //The uniqueChatID may depend upon the account, so clear it
139                 [self clearUniqueChatID];
140                 [[adium notificationCenter] postNotificationName:Chat_SourceChanged object:self]; //Notify
141         }
144 - (NSDictionary *)chatCreationDictionary
146         return [self statusObjectForKey:@"ChatCreationInfo"];
149 - (void)setChatCreationDictionary:(NSDictionary *)inDict
151         [self setStatusObject:inDict
152                                    forKey:@"ChatCreationInfo"
153                                    notify:NotifyNever];
156 - (void)accountDidJoinChat
158         [self willChangeValueForKey:@"actionMenu"];
159         [self didChangeValueForKey:@"actionMenu"];
162 //Date Opened
163 #pragma mark Date Opened
164 - (NSDate *)dateOpened
166         return dateOpened;
169 - (BOOL)isOpen
171         return isOpen;
173 - (void)setIsOpen:(BOOL)flag
175         isOpen = flag;
178 - (BOOL)hasSentOrReceivedContent
180         return hasSentOrReceivedContent;
182 - (void)setHasSentOrReceivedContent:(BOOL)flag
184         hasSentOrReceivedContent = flag;
187 //Status ---------------------------------------------------------------------------------------------------------------
188 #pragma mark Status
189 //Status
190 - (void)didModifyStatusKeys:(NSSet *)keys silent:(BOOL)silent
192         [[adium chatController] chatStatusChanged:self
193                                                    modifiedStatusKeys:keys
194                                                                            silent:silent];      
197 - (void)object:(id)inObject didSetStatusObject:(id)value forKey:(NSString *)key notify:(NotifyTiming)notify
199         //If our unviewed content changes or typing status changes, and we have a single list object, 
200         //apply the change to that object as well so it can be cleanly reflected in the contact list.
201         if ([key isEqualToString:KEY_UNVIEWED_CONTENT] ||
202                 [key isEqualToString:KEY_TYPING]) {
203                 AIListObject    *listObject = [self listObject];
204                 
205                 if (listObject) [listObject setStatusObject:value forKey:key notify:notify];
206         }
207         
208         [super object:inObject didSetStatusObject:value forKey:key notify:notify];
211 - (void)clearListObjectStatuses
213         AIListObject    *listObject = [self listObject];
214         
215         if (listObject) {
216                 [listObject setStatusObject:nil forKey:KEY_UNVIEWED_CONTENT notify:NotifyLater];
217                 [listObject setStatusObject:nil forKey:KEY_TYPING notify:NotifyLater];
218         
219                 [listObject notifyOfChangedStatusSilently:NO];
220         }
221         
223 //Secure chatting ------------------------------------------------------------------------------------------------------
224 - (void)setSecurityDetails:(NSDictionary *)securityDetails
226         [self setStatusObject:securityDetails
227                                    forKey:@"SecurityDetails"
228                                    notify:NotifyNow];
230 - (NSDictionary *)securityDetails
232         return [self statusObjectForKey:@"SecurityDetails"];
235 - (BOOL)isSecure
237         AIEncryptionStatus encryptionStatus = [self encryptionStatus];
238         
239         return (encryptionStatus != EncryptionStatus_None);
242 - (AIEncryptionStatus)encryptionStatus
244         AIEncryptionStatus      encryptionStatus = EncryptionStatus_None;
246         NSDictionary            *securityDetails = [self securityDetails];
247         if (securityDetails) {
248                 NSNumber *detailsStatus;
249                 if ((detailsStatus = [securityDetails objectForKey:@"EncryptionStatus"])) {
250                         encryptionStatus = [detailsStatus intValue];
251                         
252                 } else {
253                         /* If we don't have a specific encryption status, but do have security details, assume
254                          * encrypted and verified.
255                          */
256                         encryptionStatus = EncryptionStatus_Verified;
257                 }
258         }
260         return encryptionStatus;
263 - (BOOL)supportsSecureMessagingToggling
265         return (BOOL)[account allowSecureMessagingTogglingForChat:self];
268 //Name  ----------------------------------------------------------------------------------------------------------------
269 #pragma mark Name
270 - (NSString *)name
272         return name;
274 - (void)setName:(NSString *)inName
276         if (name != inName) {
277                 [name release]; name = [inName retain]; 
278         }
282  * @brief Return an identifier which can be used to look up this chat later
284  * Use setIdentifier to specify an arbitrary identifier for this chat.
286  * Use uniqueChatID as a unique identifier for a contact-service combination.
287  */
288 - (id)identifier
290         return identifier;
294  * @brief Set an identifier for this chat
296  * Only an account which created a chat should specify the identifier; it has no useful menaing outside that context.
297  */
298 - (void)setIdentifier:(id)inIdentifier
300         if (identifier != inIdentifier) {
301                 [identifier release];
302                 identifier = [inIdentifier retain];
303         }
306 - (NSString *)displayName
308     NSString    *outName = [self displayArrayObjectForKey:@"Display Name"];
309     return outName ? outName : (name ? name : [[self listObject] displayName]);
312 - (void)setDisplayName:(NSString *)inDisplayName
314         [[self displayArrayForKey:@"Display Name"] setObject:inDisplayName
315                                                                                            withOwner:self];
318 //Participating ListObjects --------------------------------------------------------------------------------------------
319 #pragma mark Participating ListObjects
321 - (void)addParticipatingListObject:(AIListContact *)inObject notify:(BOOL)notify
323         if (![participatingListObjects containsObjectIdenticalTo:inObject]) {
324                 //Add
325                 [participatingListObjects addObject:inObject];
327                 [[adium chatController] chat:self addedListContact:inObject notify:notify];
328         }
331 // Invite a list object to join the chat. Returns YES if the chat joins, NO otherwise
332 - (BOOL)inviteListContact:(AIListContact *)inContact withMessage:(NSString *)inviteMessage
334         return ([[self account] inviteContact:inContact toChat:self withMessage:inviteMessage]);
337 - (void)setPreferredListObject:(AIListContact *)inObject
339         preferredListObject = inObject;
342 - (AIListContact *)preferredListObject
344         return preferredListObject;
347 //If this chat only has one participating list object, it is returned.  Otherwise, nil is returned
348 - (AIListContact *)listObject
350     if (([participatingListObjects count] == 1) && ![self isGroupChat]) {
351         return [participatingListObjects objectAtIndex:0];
352     } else {
353         return nil;
354     }
356 - (void)setListObject:(AIListContact *)inListObject
358         if (inListObject != [self listObject]) {
359                 if ([participatingListObjects count]) {
360                         [participatingListObjects removeObjectAtIndex:0];
361                 }
362                 [self addObject:inListObject];
364                 //Clear any local caches relying on the list object
365                 [self clearListObjectStatuses];
366                 [self clearUniqueChatID];
368                 //Notify once the destination has been changed
369                 [[adium notificationCenter] postNotificationName:Chat_DestinationChanged object:self];
370         }
373 - (NSString *)uniqueChatID
375         if (!uniqueChatID) {
376                 if ([self isGroupChat]) {
377                         uniqueChatID = [[NSString alloc] initWithFormat:@"%@.%i",[self name],nextChatNumber++];
378                 } else {                        
379                         uniqueChatID = [[[self listObject] internalObjectID] retain];
380                 }
382                 if (!uniqueChatID) {
383                         uniqueChatID = [[NSString alloc] initWithFormat:@"UnknownChat.%i",nextChatNumber++];
384                         NSLog(@"Warning: Unknown chat %p",self);
385                 }
386         }
388         return (uniqueChatID);
391 - (void)clearUniqueChatID
393         [uniqueChatID release]; uniqueChatID = nil;
396 //Content --------------------------------------------------------------------------------------------------------------
397 #pragma mark Content
400  * @brief Informs the chat that the core and the account are ready to begin filtering and sending a content object
402  * If there is only one object in pendingOutgoingContentObjects after adding inObject, we should send immedaitely.
403  * However, if other objects are in it, we should wait for them to be removed, as they are chronologically first.
404  * If we are asked if we should begin sending the earliest object in pendingOutgoingContentObjects, the answer is YES.
406  * @param inObject The object being sent
407  * @result YES if the object should be sent immediately; NO if another object is in process so we should wait
408  */
409 - (BOOL)willBeginSendingContentObject:(AIContentObject *)inObject
411         int     currentIndex = [pendingOutgoingContentObjects indexOfObjectIdenticalTo:inObject];
413         //Don't add the object twice when we are called from -[AIChat finishedSendingContentObject]
414         if (currentIndex == NSNotFound) {
415                 [pendingOutgoingContentObjects addObject:inObject];             
416         }
418         return (([pendingOutgoingContentObjects count] == 1) ||
419                         (currentIndex == 0));
423  * @brief Informs the chat that an outgoing content object was sent and dispalyed.
425  * It is no longer pending, so we remove it from that array.
426  * If there are more pending objects, trigger sending the next.
428  * @param inObject The object with which we are finished
429  */
430 - (void)finishedSendingContentObject:(AIContentObject *)inObject
432         [pendingOutgoingContentObjects removeObjectIdenticalTo:inObject];
433         
434         if ([pendingOutgoingContentObjects count]) {
435                 [[adium contentController] sendContentObject:[pendingOutgoingContentObjects objectAtIndex:0]];
436         }
439 - (BOOL)canSendMessages
441         BOOL canSendMessages;
442         if ([self isGroupChat]) {
443                 //XXX Liar!
444                 canSendMessages = YES;
446         } else {
447                 if ([[self account] online]) {
448                         AIListContact *listObject = [self listObject];
449                         
450                         canSendMessages = ([listObject online] ||
451                                                            [listObject isStranger] ||
452                                                            [[self account] canSendOfflineMessageToContact:listObject]);
453                 } else {
454                         canSendMessages = NO;
455                 }
456         }
457         
458         return canSendMessages;
461 - (BOOL)canSendImages
463         return [[self account] canSendImagesForChat:self];
466 - (int)unviewedContentCount
468         return [self integerStatusObjectForKey:KEY_UNVIEWED_CONTENT];
471 - (void)incrementUnviewedContentCount
473         int currentUnviewed = [self integerStatusObjectForKey:KEY_UNVIEWED_CONTENT];
474         [self setStatusObject:[NSNumber numberWithInt:(currentUnviewed+1)]
475                                          forKey:KEY_UNVIEWED_CONTENT
476                                          notify:NotifyNow];
479 - (void)clearUnviewedContentCount
481         [self setStatusObject:nil forKey:KEY_UNVIEWED_CONTENT notify:NotifyNow];
484 #pragma mark AIContainingObject protocol
485 //AIContainingObject protocol
486 - (NSArray *)containedObjects
488         return participatingListObjects;
491 - (unsigned)containedObjectsCount
493         return [[self containedObjects] count];
496 - (BOOL)containsObject:(AIListObject *)inObject
498         return [[self containedObjects] containsObjectIdenticalTo:inObject];
501 - (id)objectAtIndex:(unsigned)index
503         return [[self containedObjects] objectAtIndex:index];
506 - (int)indexOfObject:(AIListObject *)inObject
508     return [[self containedObjects] indexOfObject:inObject];
511 //Retrieve a specific object by service and UID
512 - (AIListObject *)objectWithService:(AIService *)inService UID:(NSString *)inUID
514         NSEnumerator    *enumerator = [[self containedObjects] objectEnumerator];
515         AIListObject    *object;
516         
517         while ((object = [enumerator nextObject])) {
518                 if ([inUID isEqualToString:[object UID]] && [object service] == inService) break;
519         }
520         
521         return object;
524 - (NSArray *)listContacts
526         return [self containedObjects];
529 - (BOOL)addObject:(AIListObject *)inObject
531         if ([inObject isKindOfClass:[AIListContact class]]) {
532                 [self addParticipatingListObject:(AIListContact *)inObject notify:YES];
533                 
534                 return YES;
535         } else {
536                 return NO;
537         }
540 - (void)removeObject:(AIListObject *)inObject
542         if ([inObject isKindOfClass:[AIListContact class]] && [participatingListObjects containsObjectIdenticalTo:inObject]) {
543                 [participatingListObjects removeObject:inObject];
544                         
545                 [[adium chatController] chat:self removedListContact:(AIListContact *)inObject];
546         }
549 - (void)removeAllObjects 
551         while([self containedObjectsCount] > 0)
552                 [self removeObject:[self objectAtIndex:0]];
555 - (void)setExpanded:(BOOL)inExpanded
557         expanded = inExpanded;
559 - (BOOL)isExpanded
561         return expanded;
564 - (unsigned)visibleCount
566         return [self containedObjectsCount];
569 - (NSString *)contentsBasedIdentifier
571         return [NSString stringWithFormat:@"%@-%@.%@",[self name], [[self account] serviceID], [[self account] UID]];
575 //Not used
576 - (float)smallestOrder { return 0; }
577 - (float)largestOrder { return 1E10; }
578 - (void)listObject:(AIListObject *)listObject didSetOrderIndex:(float)inOrderIndex {};
581 #pragma mark    
583  * @brief Set the ignored state of a contact
585  * @param inContact The contact whose state is to be changed
586  * @param isIgnored YES to ignore the contact; NO to not ignore the contact
587  */
588 - (void)setListContact:(AIListContact *)inContact isIgnored:(BOOL)isIgnored
590         //Create ignoredListContacts if needed
591         if (isIgnored && !ignoredListContacts) {
592                 ignoredListContacts = [[NSMutableSet alloc] init];      
593         }
595         if (isIgnored) {
596                 [ignoredListContacts addObject:inContact];
597         } else {
598                 [ignoredListContacts removeObject:inContact];           
599         }       
603  * @brief Is the passed object ignored?
605  * @param inContact The contact to check
606  * @result YES if the contact is ignored; NO if it is not
607  */
608 - (BOOL)isListContactIgnored:(AIListObject *)inContact
610         return [ignoredListContacts containsObject:inContact];
613 #pragma mark Comparison
614 - (BOOL)isEqual:(id)inChat
616         return (inChat == self);
619 #pragma mark Debugging
620 - (NSString *)description
622         return [NSString stringWithFormat:@"%@:%@",
623                 [super description],
624                 (uniqueChatID ? uniqueChatID : @"<new>")];
627 #pragma mark Group Chat
629 - (void)setIsGroupChat:(BOOL)flag
631         isGroupChat = flag;
634 - (BOOL)isGroupChat
636         return isGroupChat;
639 #pragma mark Custom emoticons
641 - (void)addCustomEmoticon:(AIEmoticon *)inEmoticon
643         if (!customEmoticons) customEmoticons = [[NSMutableSet alloc] init];
644         [customEmoticons addObject:inEmoticon];
647 - (NSSet *)customEmoticons;
649         return customEmoticons;
652 #pragma mark Errors
655  * @brief Inform the chat that an error occurred
657  * @param type An NSNumber containing an AIChatErrorType
658  */
659 - (void)receivedError:(NSNumber *)type
661         //Notify observers
662         [self setStatusObject:type forKey:KEY_CHAT_ERROR notify:NotifyNow];
664         //No need to continue to store the NSNumber
665         [self setStatusObject:nil forKey:KEY_CHAT_ERROR notify:NotifyNever];
668 #pragma mark Room commands
669 - (NSMenu *)actionMenu
670 {       
671         return [[self account] actionsForChat:self];
673 - (void)setActionMenu:(NSMenu *)inMenu {};
675 #pragma mark Applescirpt
677 - (NSScriptObjectSpecifier *)objectSpecifier
679         //the chat may not be in a window! Just reference it from the application...
680         //get my window
681         NSScriptClassDescription *containerClassDesc = (NSScriptClassDescription *)[NSScriptClassDescription classDescriptionForClass:[NSApp class]];
682         return [[[NSUniqueIDSpecifier allocWithZone:[self zone]]
683                 initWithContainerClassDescription:containerClassDesc
684                 containerSpecifier:nil key:@"chats" uniqueID:[self uniqueChatID]] autorelease];
687 - (unsigned int)index
689         id<AIChatContainer> messageTab = [self statusObjectForKey:@"MessageTabViewItem"];
690         //what we're going to do is find this tab in the tab view's hierarchy, so as to get its index
691         id<AIChatWindowController> windowController = [messageTab windowController];
693         NSArray *chats = [windowController containedChats];
694         for (unsigned int i=0;i<[chats count];i++) {
695                 if ([chats objectAtIndex:i] == self)
696                         return i+1; //one based
697         }
698         NSAssert(NO, @"This chat is weird.");
699         return 0;
701 /*- (void)setIndex:(unsigned int)index
703         id<AIChatContainer> messageTab = [self statusObjectForKey:@"MessageTabViewItem"];
704         id<AIChatWindowController> windowController = [messageTab windowController];
705         NSArray *chats = [windowController containedChats];
706         NSAssert (index-1 < [chats count], @"Don't let index be bigger than the count!");
707         NSLog(@"Trying to move %@ in %@ to %u",messageTab,window,index-1);
708         [windowController moveTabViewItem:messageTab toIndex:index-1]; //This is bad bad bad. Why?
709         
712 - (NSWindow *)window
714         id<AIChatContainer> messageTab = [self statusObjectForKey:@"MessageTabViewItem"];
715         id<AIChatWindowController> windowController = [messageTab windowController];
716         return [windowController window];
719 - (id)handleCloseScriptCommand:(NSCloseCommand *)closeCommand
721         [[[AIObject sharedAdiumInstance] interfaceController] closeChat:self];
722         return nil;
725 - (void)setUniqueChatID:(NSString *)str
727         [[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
730 - (AIAccount *)scriptingAccount
732         return [self account];
735 - (void)setScriptingAccount:(AIAccount *)a
737         [[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
738         [[NSScriptCommand currentCommand] setScriptErrorString:@"Can't set the account of a chat."];
741 - (NSString *)content
743         /*AITranscriptLogEnumerator *e = [[[AITranscriptLogReader alloc] initWithChat:self] autorelease];
744         AIContentMessage *m;
745         NSMutableString *result = [[[NSMutableString alloc] init] autorelease];
746         while ((m = [e nextObject])) {
747                 [result appendFormat:@"%@\n",[m messageString]];
748         }
749         return result;*/
750         [[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
751         [[NSScriptCommand currentCommand] setScriptErrorString:@"Still unsupported."];
752         return nil;
756  * @brief Applescript command to send a message in this chat
757  */
758 - (id)sendScriptCommand:(NSScriptCommand *)command {
759         NSDictionary    *evaluatedArguments = [command evaluatedArguments];
760         NSString                *message = [evaluatedArguments objectForKey:@"message"];
761         NSURL                   *fileURL = [evaluatedArguments objectForKey:@"withFile"];
762         
763         //Send any message we were told to send
764         if (message && [message length]) {
765                 //Take the string and turn it into an attributed string (in case we were passed HTML)
766                 NSAttributedString  *attributedMessage = [AIHTMLDecoder decodeHTML:message];
767                 AIContentMessage        *messageContent;
768                 messageContent = [AIContentMessage messageInChat:self
769                                                                                           withSource:[self account]
770                                                                                          destination:[self listObject]
771                                                                                                         date:nil
772                                                                                                  message:attributedMessage
773                                                                                            autoreply:NO];
774                 
775                 [[adium contentController] sendContentObject:messageContent];
776         }
777         
778         //Send any file we were told to send to every participating list object (anyone remember the AOL mass mailing zareW scene?)
779         if (fileURL && [[fileURL path] length]) {
780                 NSEnumerator    *enumerator = [[self containedObjects] objectEnumerator];
781                 AIListContact   *listContact;
782                 
783                 while ((listContact = [enumerator nextObject])) {
784                         AIListContact   *targetFileTransferContact;
785                         
786                         //Make sure we know where we are sending the file by finding the best contact for
787                         //sending CONTENT_FILE_TRANSFER_TYPE.
788                         targetFileTransferContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
789                                                                                                                                                                    forListContact:listContact];
790                         [[adium fileTransferController] sendFile:[fileURL path]
791                                                                            toListContact:targetFileTransferContact];
792                 }
793         }
794         
795         return nil;
798 @end